<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available through the world-wide-web at                              |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net>                              |
// +----------------------------------------------------------------------+
//
// $Id: Element.php,v 1.5 2003/12/14 19:16:37 xnoguer Exp $

/**
* This class defines DICOM file elements
*
* @author   Xavier Noguer <xnoguer@php.net>
* @package  File_DICOM
* 
* ChangeLog:
* 2007.07.30: Added support for encapsulated data. Transfer Syntax:
*             "1.2.840.10008.1.2.4.70" : Lossless JPEG.
*/
class File_DICOM_Element
{
    /**
    * Type of VR for this element
    * @var integer
    */
    var $vr_type;

    /**
    * Element length
    * @var integer
    */
    var $value;

    /**
    * Element length
    * @var integer
    */
    var $code;

    /**
    * Element length
    * @var integer
    */
    var $length;

    /**
    * Complete header of this element. It might disappear in the future.
    * @var string
    */
    var $header;

    /**
    * Group this element belongs to
    * @var integer
    */
    var $group;

    /**
    * Element identifier
    * @var integer
    */
    var $element;

    /**
    * Position inside the current field for the element
    * @var integer
    */
    var $offset;
 
    /**
    * Name for this element
    * @var string
    */
    var $name;

    /**
    * Transfer Syntax. To be able of reading different types according to standard
    * 1.2.840.10008.1.2    Implicit VR, Little Endian
	* 1.2.840.10008.1.2.1  Explicit VR, Little Endian
	* 1.2.840.10008.1.2.2  Explicit VR, Big Endian
    * @var string
    */
    var $transfer_syntax;

    var $_elements;
    
    /**
    * Create DICOM file element from contents of the file given.
    * It assumes the element begins at the current position of the given 
    * file pointer.
    *
    * @param resource $IN              File handle for the file currently being parsed
    * @param array    &$dictref        Reference to the dictionary of DICOM headers
    * @param string   $transfer_syntax The transfer syntax to use when reading the Element
    * @param integer  $last_group      The last group that we read
    * @access public
    */
    function File_DICOM_Element($IN, &$dictref, $transfer_syntax, $last_group, $item_from_sequence = false)
    {
        // Assign Transfer Syntax UID
        $this->transfer_syntax = $transfer_syntax;

        // The standard says that we need to read all the elements of the 0x0002 group and after that apply the proper
        // transfer syntax. The tricky part is after you have read the [0x0002][0x0010] but still need to read the
        // rest of elements in group 0x0002 as Explicit VR Little Endian 
        if ($last_group <= 0x0002) {
            $offset  = ftell($IN);
            // Reading the group value
            $test_group = $this->_readInt($IN, 2, 2);
            // Testing if group is either 0x000 or 0x0002
            if ($test_group == 0x0000 || $test_group == 0x0002 || $test_group == 0x0200) {
                $this->transfer_syntax = EXPLICIT_VR_LITTLE_ENDIAN;
            }
            // If not, then we have passed the 0x0002 group and new elements should be read according to the transfer 
            // syntax stored in file
            fseek($IN, $offset);
        }

        // Tag holds group and element numbers in two bytes each.
        $offset  = ftell($IN);
        $group   = $this->_readInt($IN, 2, 2, "check_syntax");
        $element = $this->_readInt($IN, 2, 2, "check_syntax");

        if (isset($dictref[$group][$element])) {
            list($code, $numa, $name) = $dictref[$group][$element];
        } else {
            list($code, $numa, $name) = array("--", "UNKNOWN", "UNKNOWN");
        }
        
        if (!$item_from_sequence && $code == "NONE") {
            $code = "ITEM";
        }

        // Next 4 bytes are either explicit VR or length (implicit VR).
        $length = $this->_readLength($IN, $code);
  
        // Go to record start, read bytes up to value field, store in header.
        $diff = ftell($IN) - $offset;
        fseek($IN, $offset);
        $header = "";
        if ($diff > 0) {
            $header = fread($IN, $diff);
        }
  
        // Read in the value field.  Certain fields need to be decoded.
        $value = '';
        //echo $code . " " . $numa . " " . $name . " " . $group . " " . $element . " " . $length . "\n";
        //if ($length == 1179258881) 
        //     exit;
        if ($length >= 0) {
            switch ($code) {
                // Decode ints and shorts.
                case 'UL':
                    $value = $this->_readInt($IN, 4, $length, "check_syntax");
                    break;
                case 'US':
                    $value = $this->_readInt($IN, 2, $length, "check_syntax");
                    break;
                case 'FL':
                    // TODO: test this
                    $value = $this->_readFloat($IN, 4, $length);
                    break;
                case 'FD':
                    // TODO: test this
                    $value = $this->_readFloat($IN, 8, $length);
                    break;
                // Binary data. Only save position. Is this right? 
                case 'OW':
                case 'OB':
                case 'OX':
                case 'ITEM':
                    $value = ftell($IN);
                    fseek($IN, $length, SEEK_CUR);
                    break;
                // It is a Sequence or Item or ItemDelimitationItem or SequenceDelimitationItem
                // Save position to value and read next elements
                case 'SQ':
                case 'NONE':
                    $value = ftell($IN);
                	while (ftell($IN) < $value + $length) {
	                	// New element. We are passing the current transfer syntax and a $last_group value of 0xFFFE
	                	$new_element =& new File_DICOM_Element($IN, $dictref, $transfer_syntax, 0xFFFE, true);
    	            	$this->_elements[$new_element->group][$new_element->element][] =& $new_element;
        	    		$this->_elements_by_name[$new_element->name][] =& $new_element;
                	}
            		break;
                default: // Made it to here: Read bytes verbatim.
                    $value = "";
                    if ($length > 0) {
                        $value = fread($IN, $length);
                    }
                    $value = $this->trimValue($value);
                    break;
            }
        }
 
        // Fill in hash of values and return them.
        $this->value  = $value;
        $this->code   = $code;
        $this->length = $length;
        // why save header??
        $this->header  = $header;
        $this->element = $element;
        $this->group   = $group;
        $this->name    = $name;
        $this->offset  = $offset;
    }

    /**
    * Return the Value Field length, and length before Value Field.
    * Implicit VR: Length is 4 byte int.
    * Explicit VR: 2 bytes hold VR, then 2 byte length.
    *
    * @param resource $IN File handle for the file currently being parsed
    * @access private
    * @return integer The length for the current field
    */
    function _readLength($IN, $code)
    {
		global $VR_array;    	
        // Read 4 bytes into b0, b1, b2, b3.
        $buff = fread($IN, 4);
        if (strlen($buff) < 4) {
            return 0;
        }
        $b = unpack("C4", $buff);
        // Temp string to test for explicit VR
        $vrstr = pack("C", $b[1]) . pack("C", $b[2]);
        # Assume that this is explicit VR if b[1] and b[2] match a known VR code.
        # Possibility (prob 26/16384) exists that the two low order field length 
        # bytes of an implicit VR field will match a VR code.
        # FIXED: Now it needs to check the right value from the "code".
        # TODO: Multiple options for "code"
  
        # DICOM PS 3.5 Sect 7.1.2: Data Element Structure with Explicit VR
        # Explicit VRs store VR as text chars in 2 bytes.
        # VRs of OB, OW, SQ, UN, UT have VR chars, then 0x0000, then 32 bit VL:
        #
        # +-----------------------------------------------------------+
        # |  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |  8 |  9 | 10 | 11 |
        # +----+----+----+----+----+----+----+----+----+----+----+----+
        # |<Group-->|<Element>|<VR----->|<0x0000->|<Length----------->|<Value->
        #
        # Other Explicit VRs have VR chars, then 16 bit VL:
        #
        # +---------------------------------------+
        # |  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |
        # +----+----+----+----+----+----+----+----+
        # |<Group-->|<Element>|<VR----->|<Length->|<Value->
        #
        # Implicit VRs have no VR field, then 32 bit VL:
        #
        # +---------------------------------------+
        # |  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |
        # +----+----+----+----+----+----+----+----+
        # |<Group-->|<Element>|<Length----------->|<Value->
  
        if (array_key_exists($vrstr,$VR_array)) {
            if (($vrstr == $code) || ($code == "--") || (substr($code,0,1) == "X") || (substr($code,1,1) == "X")) {
                // Have a code for an explicit VR: Retrieve VR element
                list($name, $bytes, $fixed) = $VR_array[$vrstr];
                if ($bytes == 0) {
                    $this->vr_type = FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS;
                    // This is an OB, OW, SQ, UN or UT: 32 bit VL field.
                    // Have seen in some files length 0xffff here...
                    // Looking for:
                    // unsigned long (always 32 bit, little endian byte order)
                    return $this->_readInt($IN, 4, 4, "check_syntax");
                } else {
                    // This is an explicit VR with 16 bit length.
                    $this->vr_type = FILE_DICOM_VR_TYPE_EXPLICIT_16_BITS;
			        // Here for Explicit VR Big Endian the length is read differently
			        if ($this->transfer_syntax == EXPLICIT_VR_BIG_ENDIAN) {
			            return ($b[3] << 8) + $b[4];
			        }
            	    return ($b[4] << 8) + $b[3];
                }
            }
        }
        // Made it to here: Implicit VR, 32 bit length.
        $this->vr_type = FILE_DICOM_VR_TYPE_IMPLICIT;
        // Here for Explicit VR Big Endian the length is read differently
        if ($this->transfer_syntax == EXPLICIT_VR_BIG_ENDIAN) {
            return ($b[1] << 24) + ($b[2] << 16) + ($b[3] << 8) + $b[4];
        }
        return ($b[4] << 24) + ($b[3] << 16) + ($b[2] << 8) + $b[1];
    }

    /**
    * Read an integer field from a file handle
    * If $len > $bytes multiple values are read in and
    * stored as a string representation of an array.
    * This method will probably change in the future.
    *
    * @access private
    * @param resource $IN filehandle for the file currently being parsed
    * @param integer  $bytes Number of bytes for integer (2 => short, 4 => integer)
    * @param integer  $len   Optional total number of bytes on the field
    * @param string   $type  Either "check_syntax" or "no_check_syntax". 
    * @return mixed integer value if $len == $bytes, an array of integer if $len > $bytes
    */
    function _readInt($IN, $bytes, $len, $type = "no_check_syntax")
    {
        $format = ($bytes == 2) ? "v" : "V";
  
        $buff = fread($IN, $len);
        if ($this->transfer_syntax == EXPLICIT_VR_BIG_ENDIAN && $type == "check_syntax")
        	$buff = strrev($buff);
        if ($len == $bytes) {
            if (strlen($buff) > 0) {
                $val = unpack($format, $buff);
                //return $val[''];
                return current($val);
            } else {
                return '';
            }
        } else {
            // Multiple values: Create array.
            // Change this!!!
            $vals = array();
            for ($pos = 0; $pos < $len; $pos += $bytes) {
                //$unpacked = unpack("$format", substr($buff, $pos, $bytes));
                $unpacked = unpack($format, substr($buff, $pos, $bytes));
                //$vals[] = $unpacked[''];
                //var_dump($unpacked);
                $vals[] = current($unpacked);
            }
            $val = "[" . join(", ", $vals) . "]";
            return $val;
        }
    }

    /**
    * Read a float field from a file handle
    * If $len > $bytes multiple values are read in and
    * stored as a string representation of an array.
    * This method will probably change in the future.
    *
    * @access private
    * @param resource $IN filehandle for the file currently being parsed
    * @param integer  $bytes Number of bytes for float (4 => float, 8 => double)
    * @param integer  $len   Total number of bytes on the field
    * @return mixed double value if $len == $bytes, an array of doubles if $len > $bytes
    */
    function _readFloat($IN, $bytes, $len)
    {
        $format = ($bytes == 4) ? 'f' : 'd';
  
        $buff = fread($IN, $len);
        //if ($this->transfer_syntax == EXPLICIT_VR_BIG_ENDIAN)
        //	$buff = strrev($buff);
        if ($len == $bytes) {
            if (strlen($buff) > 0) {
                $val = unpack($format, $buff);
                return $val[''];
            } else {
                return '';
            }
        } else {
            // Multiple values: Create array.
            // Change this!!!
            $vals = array();
            for ($pos = 0; $pos < $len; $pos += $bytes) {
                $unpacked = unpack("$format", substr($buff, $pos, $bytes));
                $vals[] = $unpacked[''];
            }
            $val = "[" . join(", ", $vals) . "]";
            return $val;
        }
    }

    /**
    * Retrieves the value field for this File_DICOM_Element
    *
    * @access public
    * @return mixed The value for this File_DICOM_Element
    */
    function getValue()
    {
        return $this->value;
    }

    /**
    * Sets the value field for this File_DICOM_Element
    *
    * @access public
    * @return boolean True is successfull, False if failed
    */
    function setValue()
    {
        return $this->value;
    }
    
    /**
    * Takes out extra padding chr(0) from value.
    *
    * @access public
    * @param string   $value The string to be trimmed
    * @return string  The trimmed value
    */
    function trimValue($value)
	{
		if (substr($value,-1) == chr(0))
			return substr($value,0,-1);
		return $value; 
	}

    /**
    * Adds padding chr(0) at the end if not an even length.
    *
    * @access public
    * @param string   $value The string to be padded
    * @return string  The padded value
    */
    function padValue($value)
	{
		if (strlen($value) % 2 != 0)
			return $value . chr(0);
		return $value; 
	}
}
?>
